/**
 * 
 */
package jspeech.sys;

import java.io.*;
import java.net.*;

import java.net.SocketTimeoutException;
import java.util.concurrent.*;

import jcommand.CommandSet;
import jspeech.exception.*;

/**
 * <code>LinkLayer</code> object brings to create a socket to link with the
 * TST(texttospeech) engine and sound recognition engine.
 * <p>
 * It will send command and receive commend with using the socket.
 * <p>
 * <i>TST(texttospeech) engine and sound recognition engine are both implemented
 * by SAPI 5.0.</i>
 * <p>
 * &nbsp;
 * <p>
 * <b>Workflow when running <code>start()</code>: </b>
 * <p>
 * <table>
 * <tr>
 * <td>Step 1:</td>
 * <td>Create a Server Socket</td>
 * </tr>
 * <tr>
 * <td>&nbsp;&nbsp;Exception:</td>
 * <td><code>throw new SDKConnectionException</code></td>
 * </tr>
 * <tr>
 * <td>Step 2:</td>
 * <td>Execute the .exe of the engine</td>
 * </tr>
 * <tr>
 * <td>&nbsp;&nbsp;Exception:</td>
 * <td><code>throw new SDKNotFoundException</code></td>
 * </tr>
 * <tr>
 * <td>Step 3:</td>
 * <td>Wait until the socket connected between the engine and the
 * <code>LinkLayer</code></td>
 * </tr>
 * <tr>
 * <td>&nbsp;&nbsp;Exception:</td>
 * <td><code>throw new SocketTimeoutException</code></td>
 * <tr>
 * <td>&nbsp;</td>
 * <td><code>throw new EngineConnectionException</code></td>
 * </tr>
 * <tr>
 * <td>Step 4:</td>
 * <td>Create <code>PrintWriter</code> and <code>BufferReader</code> to
 * send and receive the data.</td>
 * </tr>
 * <tr>
 * <td>&nbsp;&nbsp;Exception:</td>
 * <td><code>throw new SDKConnectionException</code></td>
 * </tr>
 * <tr>
 * <td>Step 5:</td>
 * <td>The connection is established. Ready to read / write the data.</td>
 * </tr>
 * </table>
 * <p>
 * &nbsp;
 * <p>
 * 
 * @author Kenneth Lo
 * 
 */
public class LinkLayer {

	/**
	 * A server socket waits for requests to come in over the TST engine and
	 * recognition engine. It performs some operation based on that request, and
	 * then possibly returns a result to the requester.
	 */
	private ServerSocket serverSocket = null;

	/**
	 * A socket is an endpoint for communication between JSpeech and the engine.
	 */
	private Socket clientSocket = null;

	/**
	 * Write data to the socket (JSpeech to Engine)
	 */
	private PrintWriter out = null;

	/**
	 * A thread keeping reading the socket
	 */
	private SocketReader socketReader;

	/**
	 * Execute {@link jspeech.sys.SocketReader}
	 */
	private ExecutorService executor = Executors.newFixedThreadPool(1);

	/**
	 * Run the recohnizer in background
	 */
	private boolean backgroundProcess;

	/**
	 * If this set to true, the engine will load from the path set from the
	 * config file, otherwise, the engines should start manually.
	 */
	private boolean load_engine_from_config;

	/**
	 * If the debug mode is set to true, detail flow message will print out to
	 * the console.
	 */
	private boolean debug_mode;

	/**
	 * Port number of the socket.
	 */
	final protected int port;

	/**
	 * Create the LinkLayer object, bound to the specified port.
	 * 
	 * @param port
	 *            port the port number
	 */
	public LinkLayer(int port, boolean backgroundProcess,
			boolean load_engine_from_config, boolean debug_mode) {
		this.port = port;
		this.backgroundProcess = backgroundProcess;
		this.load_engine_from_config = load_engine_from_config;
		this.debug_mode = debug_mode;
	}

	/**
	 * Start to establish the connection between the JSpeech and the engine
	 * 
	 * @param engine
	 *            The full path of the executable of the engine
	 * @param timeout
	 *            the specified timeout, in milliseconds
	 * @param commandSet
	 *            commands that need to run
	 * @throws EngineConnectionException
	 *             if JSpeech fail to establish the connection with the engine
	 * @throws EngineNotFoundException
	 *             if the executable of the engine cannot be found
	 * @throws TimeoutException
	 *             if the engine have not send back the reply after the timeout
	 *             period
	 */
	public void start(String engine, int timeout, CommandSet commandSet)
			throws EngineConnectionException, EngineNotFoundException,
			jspeech.exception.TimeoutException {

		if (this.debug_mode) {
			System.out.println("Parameters:");
			System.out.println(" > Engine: " + engine);
			System.out.println(" > Port: " + port);
			System.out.println(" > Timeout: " + timeout);
			System.out.println(" > Background Process: " + backgroundProcess);
			System.out.println(" > LOAD_ENGINE_FROM_CONFIG: "
					+ this.load_engine_from_config);

			if (commandSet == null)
				System.out.println(" > CommandSet: false");
			else
				System.out.println(" > CommandSet: true");
			System.out.println("============= Init Start =============");
		}

		// Step 1: Create a Server Socket
		try {
			serverSocket = new ServerSocket(this.port);
			serverSocket.setSoTimeout(timeout);
			if (this.debug_mode)
				System.out.println("Starting ServerSocket with port "
						+ serverSocket.getLocalPort());

		} catch (IOException e) {
			if (this.debug_mode)
				System.out.println("Fail to start");
			throw new EngineConnectionException("Could not listen on port: "
					+ port);
		}

		// Step 2: Execute the .exe of the engine
		if (this.load_engine_from_config && this.debug_mode)
			System.out.println("Starting engine " + engine);

		if (this.load_engine_from_config) {
			try {
				Runtime.getRuntime().exec(engine + " " + port);
			} catch (IOException e1) {
				if (this.debug_mode)
					System.out.println("Fail to start engine");
				throw new EngineNotFoundException(engine);
			}
		}

		// Step 3: Wait until the socket connected between the engine and the
		// JSpeech
		if (this.debug_mode)
			System.out.println("Waiting engine to connect.");
		try {
			clientSocket = serverSocket.accept();
		} catch (SocketTimeoutException ste) {
			throw new jspeech.exception.TimeoutException();
		} catch (IOException e) {
			throw new EngineConnectionException("Fail to accept with port "
					+ port);
		}

		// Step 4: Create PrintWriter and BufferReader to send and receive the
		// data.
		if (this.debug_mode)
			System.out.println("Engine connected");

		try {
			out = new PrintWriter(clientSocket.getOutputStream(), true);

			if (commandSet == null)
				socketReader = new SocketReader(clientSocket,
						backgroundProcess, this.debug_mode);
			else
				socketReader = new SocketReader(clientSocket,
						backgroundProcess, commandSet, this.debug_mode);

			executor.execute(this.socketReader);

		} catch (IOException e) {
			throw new EngineConnectionException("Fail to init Writer/Reader", e);
		}

		if (this.debug_mode) {
			System.out.println("Ready to read / write");
			System.out.println("============= Init Completed =============");
		}

		// Step 5: The connection is established. Ready to read / write the
		// data.
	}

	/**
	 * Start to establish the connection between the JSpeech and the engine
	 * 
	 * @param engine
	 *            The full path of the executable of the engine
	 * @param timeout
	 *            the specified timeout, in milliseconds
	 * @throws EngineConnectionException
	 *             if JSpeech fail to establish the connection with the engine
	 * @throws EngineNotFoundException
	 *             if the executable of the engine cannot be found
	 * @throws TimeoutException
	 *             if the engine have not send back the reply after the timeout
	 *             period
	 */
	public void start(String engine, int timeout)
			throws EngineConnectionException, EngineNotFoundException,
			jspeech.exception.TimeoutException {
		start(engine, timeout, null);
	}

	/**
	 * Closes this socket. Any thread currently blocked in accept() will throw a
	 * SocketException.
	 * <p>
	 * If this socket has an associated channel then the channel is closed as
	 * well.
	 * 
	 * @throws IOException
	 *             if an I/O error occurs when closing the socket.
	 */
	public void close() throws IOException {
		out.close();

		socketReader.close();
		executor.shutdown();
		serverSocket.close();
		clientSocket.close();
	}

	/**
	 * Write data to the socket (JSpeech to Engine)
	 * 
	 * @param text
	 * @return true if the data has been send to the socket
	 */
	public boolean writeToSocket(String text) {
		try {
			if (this.debug_mode)
				System.out.println(" >WRITE:" + text);
			out.println(text);
			return true;
		} catch (RuntimeException e) {
			return false;
		}
	}

	/**
	 * Read data from the socket
	 * 
	 * @return a string representation of data send from the engine
	 * @throws EngineConnectionException
	 */
	public String readFromSocket() throws EngineConnectionException {

		// If the program is running in foreground (backgroundProcess =
		// false), then, check the received message. If the message is null,
		// sleep 500ms and wait until the message is nit return null

		if (clientSocket.isConnected()) {

			String message = socketReader.getSocketMessage();

			if (this.backgroundProcess)
				return message;
			else {
				while (message == null) {
					try {
						Thread.sleep(50);
					} catch (InterruptedException e) {

					}
					message = socketReader.getSocketMessage();
				}

				return message;
			}
		}
		throw new EngineConnectionException("Socket is closed");
	}

	/**
	 * Return the connection status of the socket
	 * 
	 * @return true if the socket is connected, otherwise false
	 */
	public boolean isConnected() {
		if (serverSocket.isClosed())
			return false;

		if (clientSocket.isClosed())
			return false;

		return true;
	}
}
